package com.legsem.jhosync;

import java.sql.SQLException;
import java.util.Map;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.legsem.jhosync.change.ChangesSet;
import com.legsem.jhosync.change.CreateErrors;
import com.legsem.jhosync.change.CreateLinks;
import com.legsem.jhosync.change.DeleteErrors;
import com.legsem.jhosync.change.Deletes;
import com.legsem.jhosync.change.Inserts;
import com.legsem.jhosync.change.SourceErrors;
import com.legsem.jhosync.change.UpdateErrors;
import com.legsem.jhosync.model.Client;
import com.legsem.jhosync.model.ClientDoc;
import com.legsem.jhosync.util.TokenUtil;

/**
 * This is a proxy that represents a Client in a sync request process.
 * <p/>
 * The proxy holds the client document which is a copy of the data held by the
 * device.
 * 
 */
public class ClientSync {

    /** Holds a copy of the client version of the data. */
    private ClientDoc clientDoc;

    /**
     * Maximum size of a page of data that can be sent back to the client. This
     * is expressed as a maximum number of keys, not bytes.
     */
    private int pSize;

    private SourceSync sourceSync;

    final Logger logger = LoggerFactory.getLogger(ClientSync.class);

    /**
     * @param app_name the application name
     * @param client the client object
     * @param sourceConfig the target source configuration
     * @param p_size the page size
     * @param context the session and configuration parameters
     * @throws ServerException if source is not configured properly
     */
    public ClientSync(String app_name, Client client,
            SourceConfig sourceConfig, int p_size, Context context)
            throws ServerException {

        try {
            clientDoc = ClientDoc.find(client.getId(), app_name,
                    sourceConfig.getName(), client.getUserLogin());
            if (clientDoc == null) {
                clientDoc = new ClientDoc(client.getId(), app_name,
                        sourceConfig.getName(), client.getUserLogin());
                clientDoc.create();
            }
            this.pSize = p_size;
            sourceSync = new SourceSync(app_name, client.getUserLogin(),
                    sourceConfig, context);
        } catch (SQLException e) {
            throw new ServerException(e);
        }
    }

    /**
     * Query entities from the backend.
     * <p/>
     * If the client is sending a valid token (or no token), the query request
     * is processed. Otherwise something went wrong (since client did not ack)
     * and the data is resent.
     * <p/>
     * TODO resending on wrong token is dangerous and may lead to a loop between
     * client and server...
     * <p/>
     * Starts by asking the source to synchronize with the backend. This ensures
     * the master document is up to date.
     * 
     * @param token relates to the previous replay that the client has
     *            successfully processed
     * @param queryParams the query parameters
     * @return pages of updates the client needs to perform as a result of the
     *         query
     * @throws SQLException storage failure
     */
    public ClientUpdatePages query(String token,
            Map < String, Object > queryParams) throws SQLException {
        ClientUpdatePages res = null;
        if (ackToken(token)) {
            SourceErrors errors = getSourceSync().processQuery(queryParams);
            res = sendNewPage(errors);
        } else {
            res = resendPage();
        }
        return res;

    }

    /**
     * Update (create/update/delete) entities from the backend.
     * <p/>
     * This entails the following actions:
     * <ul>
     * <li>Update the backend and master document with the changes requested by
     * the client</li>
     * <li>Update the client document. The client document size is also updated
     * as we know these changes are already effective on the client</li>
     * <li>Store updates feedback (such as new identifiers created and errors
     * encountered) in the client document. Client will get these on the next
     * query</li>
     * </ul>
     * TODO add BLOB processing
     * 
     * There are 2 levels of maps in the request hash of hashes for each
     * operation:
     * <ul>
     * <li>The first key is the primary key on which an operation is performed</li>
     * <li>The second key is the attribute associated with a value</li>
     * </ul>
     * 
     * @param createRequests the creates requested by the client
     * @param updateRequests the updates requested by the client
     * @param deleteRequests the deletes requested by the client
     * @return the changes applied to client document as aweel as errors
     * @throws SQLException storage failure
     * 
     */
    public ChangesSet cud(
            Map < String, Map < String, Object > > createRequests,
            Map < String, Map < String, Object > > updateRequests,
            Map < String, Map < String, Object > > deleteRequests)
            throws SQLException {

        ChangesSet changes = getSourceSync().processCUD(createRequests,
                updateRequests, deleteRequests);

        clientDoc.setCd(changes.apply(clientDoc.getCd()));
        clientDoc.setCdSize(clientDoc.getCdSize() + changes.updateCount());

        clientDoc.setCreateDeletes(changes.getCreateDeletes().encode());
        clientDoc.setCreateLinks(changes.getCreateLinks().encode());
        clientDoc.setCreateErrors(changes.getCreateErrors().encode());
        clientDoc.setUpdateErrors(changes.getUpdateErrors().encode());
        clientDoc.setDeleteErrors(changes.getDeleteErrors().encode());
        clientDoc.update();

        return changes;

    }

    /*
     * ------------------------------------------------------------------------
     * Protected methods
     * ------------------------------------------------------------------------
     */
    /**
     * If there is an outstanding token to be acknowledged, then we check if the
     * proposed token matches. If it does, then this request is an a
     * acknowledgment.
     * <p/>
     * If we have an ack, we remove temporary from the client document.
     * 
     * @param token a proposed token
     * @return true if there was an outstanding token matching the proposed
     *         token
     */
    protected boolean ackToken(String token) {
        String storedToken = clientDoc.getPageToken();
        if (storedToken != null) {
            if (token != null && token.equals(storedToken)) {
                clientDoc.setPageToken(null);
                clientDoc.setCreateLinks(null);
                clientDoc.setCreateDeletes(null);
                clientDoc.setPage(null);
                clientDoc.setDeletePage(null);
                clientDoc.setMetadataPage(null);
                deleteCUDErrors();
                return true;
            } else {
                logger.debug("Client sent token {} different from expected {}",
                        token, storedToken);
                return false;
            }
        } else {
            return true;
        }
    }

    /**
     * Evaluates differences between the master document and the client
     * document. Differences translate into a set of atomic operations at the
     * key/attribute/value level.
     * <p/>
     * Extracts a page worth of inserts and deletes to send back to client. Page
     * size is limited so that updates are not all sent at the same time.
     * <p/>
     * A copy of the update pages is saved in order to resend if needed.
     * <p/>
     * The client document is updated one page at the time so that it reflects
     * the status of the client data and allows evaluating subsequent pages by
     * diffing the client and master document again.
     * <p/>
     * The client document size reflects the number of items that was
     * acknowledged by the client, not the actual number of items ion the cd
     * column.
     * <p/>
     * Each page of updates (if any) has an associated token that the client
     * needs to ack.
     * <p/>
     * Links might have been produced by a previous CUD request and needs to be
     * sent back.
     * <p/>
     * TODO The original code copies the link data in yet another column: We are
     * not renaming the "#{operation}_errors" to "#{operation}_errors_page" like
     * the original code does. Is this necessary?
     * <p/>
     * Creations might have been rejected resulting in the need to notify client
     * that corresponding entities need to be deleted. These notifications take
     * priority in the page order and are not restricted to the page size so
     * that they can be sent all at once. This might result in larger than
     * p_size pages.
     * <p/>
     * 
     * @param errors contains errors encountered by the source while processing
     *            the query
     * @return one page worth of updates operations that the client need to
     *         perform to get synched with the master document.
     * @throws SQLException storage failure
     */
    protected ClientUpdatePages sendNewPage(SourceErrors errors)
            throws SQLException {
        // TODO Schema changed logic missing
        logger.debug("Client document before updates: {} ", clientDoc.getCd());

        ChangesSet changes = new ChangesSet(sourceSync.getMasterDoc(),
                clientDoc);

        // Add changes left by previous cud operations to the change set
        changes.getCreateLinks().set(clientDoc.getCreateLinks());
        changes.getCreateDeletes().set(clientDoc.getCreateDeletes());

        // Add error reports
        changes.setSourceErrors(errors);
        addCUDErrors(changes);

        ClientUpdatePages updatesPage = new ClientUpdatePages(getPSize(),
                sourceSync.getMasterDoc().getMdSize(), changes);

        // If there are updates that the client needs to ack, send a token
        if (updatesPage.ackNeeded()) {
            String token = TokenUtil.newToken();
            updatesPage.setToken(token);
        }
        saveClientUpdates(updatesPage);

        logger.debug("New pages prepared: {}", updatesPage.toString());

        return updatesPage;

    }

    /**
     * All changes sent back to client are stored in the client document waiting
     * for an acknowledgement from the client. If none is received, this saved
     * data is used to resend the changes.
     * 
     * @param updatesPage the changes sent to client
     * @throws SQLException if client document cannot be updated
     */
    protected void saveClientUpdates(ClientUpdatePages updatesPage)
            throws SQLException {

        clientDoc.setPage(updatesPage.getPageChanges().getInserts().encode());
        clientDoc.setDeletePage(updatesPage.getPageChanges().getDeletes()
                .encode());
        clientDoc.setTotalCountPage(updatesPage.getTotalCount());

        clientDoc.setPageToken(updatesPage.getToken());

        clientDoc.setCd(updatesPage.getPageChanges().apply(clientDoc.getCd()));
        clientDoc.setCdSize(updatesPage.getProgressCount());

        clientDoc.setMetadataPage(updatesPage.getPageChanges().getMetadata());
        if (updatesPage.getPageChanges().getMetadataSha1() != null) {
            clientDoc.setMetadataSha1(updatesPage.getPageChanges()
                    .getMetadataSha1());
        }

        clientDoc.update();

        logger.debug("Client document updated: {} ", clientDoc);
    }

    /**
     * Client is not acknowledging the latest token, resend the last update
     * page.
     * 
     * @return the updates operations that the client need to perform to get
     *         synched with the master document.
     */
    protected ClientUpdatePages resendPage() {

        ChangesSet pageChangesSet = new ChangesSet();
        pageChangesSet.setInserts(new Inserts(clientDoc.getPage()));
        pageChangesSet.setDeletes(new Deletes(clientDoc.getDeletePage()));
        pageChangesSet.setCreateLinks(new CreateLinks(clientDoc
                .getCreateLinks()));

        addCUDErrors(pageChangesSet);

        pageChangesSet.setMetadata(clientDoc.getMetadataPage());
        pageChangesSet.setMetadataSha1(clientDoc.getMetadataSha1());

        ClientUpdatePages updatesPage = new ClientUpdatePages(
                clientDoc.getCdSize(), clientDoc.getTotalCountPage(),
                pageChangesSet, clientDoc.getPageToken());

        logger.debug("Resending pages: " + updatesPage.toString());

        return updatesPage;

    }

    /**
     * Add previous pending CUD error messages.
     * 
     * @param pageChangesSet
     */
    protected void addCUDErrors(ChangesSet pageChangesSet) {
        pageChangesSet.setCreateErrors(new CreateErrors(clientDoc
                .getCreateErrors()));
        pageChangesSet.setUpdateErrors(new UpdateErrors(clientDoc
                .getUpdateErrors()));
        pageChangesSet.setDeleteErrors(new DeleteErrors(clientDoc
                .getDeleteErrors()));
    }

    /**
     * Erase previous pending CUD error messages.
     */
    protected void deleteCUDErrors() {
        clientDoc.setCreateErrors(null);
        clientDoc.setUpdateErrors(null);
        clientDoc.setDeleteErrors(null);
    }

    /*
     * ------------------------------------------------------------------------
     * Properties
     * ------------------------------------------------------------------------
     */
    public ClientDoc getClientDoc() {
        return clientDoc;
    }

    public void setClientDoc(ClientDoc clientDoc) {
        this.clientDoc = clientDoc;
    }

    public int getPSize() {
        return pSize;
    }

    public void setPSize(int p_size) {
        this.pSize = p_size;
    }

    public SourceSync getSourceSync() {
        return sourceSync;
    }

}
